<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    <title>Rhythm — Space.js</title>

    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300&family=Gothic+A1:wght@400;700">
    <link rel="stylesheet" href="assets/css/style.css">

    <style>
        *, :after, :before {
            touch-action: unset;
        }

        body {
            position: unset;
            overscroll-behavior: none;
        }
    </style>

    <script type="module">
        import { BufferLoader, Interface, Panel, PanelItem, WebAudio, headsTails, ticker } from '../src/index.js';

        class Instructions extends Interface {
            constructor() {
                super('.instructions');

                this.initHTML();
            }

            initHTML() {
                this.invisible();
                this.css({
                    position: 'absolute',
                    left: '50%',
                    bottom: 55,
                    width: 300,
                    marginLeft: -300 / 2,
                    opacity: 0
                });

                this.container = new Interface('.container');
                this.container.css({
                    position: 'absolute',
                    bottom: 0,
                    width: '100%'
                });
                this.add(this.container);

                this.text = new Interface('.text');
                this.text.css({
                    fontFamily: 'Gothic A1, sans-serif',
                    fontWeight: '700',
                    fontSize: 10,
                    lineHeight: 20,
                    letterSpacing: 0.8,
                    textAlign: 'center',
                    textTransform: 'uppercase',
                    opacity: 0.7
                });
                this.text.text(`${navigator.maxTouchPoints ? 'Tap' : 'Click'} for sound`);
                this.container.add(this.text);
            }

            // Public methods

            toggle = (show, delay = 0) => {
                if (show) {
                    this.visible();
                    this.tween({ opacity: 1 }, 800, 'easeInOutSine', delay);
                    this.text.css({ y: 10 }).tween({ y: 0 }, 1200, 'easeOutCubic', delay);
                } else {
                    this.tween({ opacity: 0 }, 300, 'easeOutSine', () => {
                        this.invisible();
                    });
                }
            };
        }

        class UI extends Interface {
            constructor() {
                super('.ui');

                this.initHTML();
                this.initViews();
            }

            initHTML() {
                this.css({
                    minHeight: '100%',
                    display: 'flex',
                    justifyContent: 'center',
                    alignItems: 'center',
                    flexWrap: 'wrap',
                    gap: 20,
                    padding: '55px 0 125px',
                    pointerEvents: 'none',
                    webkitUserSelect: 'none',
                    userSelect: 'none'
                });
            }

            initViews() {
                this.instructions = new Instructions();
                this.add(this.instructions);
            }
        }

        class AudioController {
            static init(instructions) {
                this.instructions = instructions;

                this.context = WebAudio.context;
                this.lastTime = null;

                this.initSounds();

                this.addListeners();
            }

            static initSounds() {
                this.ambient = WebAudio.get('metal_monk_loop');
                this.ambient.gain.set(0.2);
                this.ambient.loop = true;
                this.ambient.play();

                this.bells = WebAudio.get('ethereal_bells');
                this.bells.gain.set(0.5);

                this.accent1 = WebAudio.get('accent_transition_1');
                this.accent1.gain.set(0.1);

                this.accent2 = WebAudio.get('accent_transition_2');
                this.accent2.gain.set(0.05);

                this.kick = WebAudio.get('kick');
                this.kick.gain.set(1);

                this.snare = WebAudio.get('snare');
                this.snare.gain.set(1);

                this.hihat = WebAudio.get('hihat');
                this.hihat.gain.set(1);
            }

            static addListeners() {
                document.addEventListener('visibilitychange', this.onVisibility);
                document.addEventListener('pointerdown', this.onPointerDown);

                this.instructions.toggle(true);
            }

            // Event handlers

            static onVisibility = () => {
                if (document.hidden) {
                    WebAudio.mute();
                } else {
                    WebAudio.unmute();
                }
            };

            static onPointerDown = () => {
                // this.instructions.toggle(false);

                // Based on https://www.html5rocks.com/en/tutorials/webaudio/intro/ by smus

                const bells = this.bells;
                const accent1 = this.accent1;
                const accent2 = this.accent2;
                const kick = this.kick;
                const snare = this.snare;
                const hihat = this.hihat;

                const tempo = 70; // BPM (beats per minute)
                const eighthNoteTime = (60 / tempo) / 2;
                const barLength = 8 * eighthNoteTime;

                // Snap to bar length
                let startTime = Math.ceil(this.context.currentTime / barLength) * barLength;

                // Next 4 bars
                const lastLength = this.lastTime + 4 * barLength;

                if (this.lastTime !== null && startTime < lastLength) {
                    startTime = lastLength;
                }

                this.lastTime = startTime;

                // Play the bells on the first eighth note
                bells.play(startTime + eighthNoteTime);

                // Play the accents on bar 2, beat 4
                if (headsTails()) {
                    accent1.play(startTime + barLength + 6 * eighthNoteTime);
                } else {
                    accent2.play(startTime + barLength + 6 * eighthNoteTime);
                }

                // Play 4 bars
                for (let bar = 0; bar < 4; bar++) {
                    // We'll start playing the rhythm one eighth note from "now"
                    const time = startTime + bar * barLength + eighthNoteTime;

                    // Play the bass (kick) drum on beats 1, 3
                    kick.play(time);
                    kick.play(time + 4 * eighthNoteTime);

                    // Play the snare drum on beats 2, 4
                    snare.play(time + 2 * eighthNoteTime);
                    snare.play(time + 6 * eighthNoteTime);

                    // Play the hi-hat every eighth note
                    for (let i = 0; i < 8; i++) {
                        hihat.play(time + i * eighthNoteTime);
                    }
                }
            };
        }

        class PanelController {
            static init(ui) {
                this.ui = ui;

                this.initPanel();
            }

            static initPanel() {
                const { ambient, bells, accent1, accent2, kick, snare, hihat } = AudioController;

                const track1 = new Panel();
                track1.animateIn();
                this.ui.add(track1);

                [
                    {
                        label: 'Ambient'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'slider',
                        label: 'Volume',
                        min: 0,
                        max: 1,
                        step: 0.01,
                        value: ambient.gain.value,
                        callback: value => {
                            ambient.gain.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Pan',
                        min: -1,
                        max: 1,
                        step: 0.01,
                        value: ambient.stereoPan.value,
                        callback: value => {
                            ambient.stereoPan.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Rate',
                        min: 0,
                        max: 2,
                        step: 0.01,
                        value: ambient.playbackRate.value,
                        callback: value => {
                            ambient.playbackRate.value = value;
                        }
                    }
                ].forEach(data => {
                    track1.add(new PanelItem(data));
                });

                const track2 = new Panel();
                track2.animateIn();
                this.ui.add(track2);

                [
                    {
                        label: 'Bells'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'slider',
                        label: 'Volume',
                        min: 0,
                        max: 1,
                        step: 0.01,
                        value: bells.gain.value,
                        callback: value => {
                            bells.gain.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Pan',
                        min: -1,
                        max: 1,
                        step: 0.01,
                        value: bells.stereoPan.value,
                        callback: value => {
                            bells.stereoPan.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Rate',
                        min: 0,
                        max: 2,
                        step: 0.01,
                        value: bells.playbackRate.value,
                        callback: value => {
                            bells.playbackRate.value = value;
                        }
                    }
                ].forEach(data => {
                    track2.add(new PanelItem(data));
                });

                const track3 = new Panel();
                track3.animateIn();
                this.ui.add(track3);

                [
                    {
                        label: 'Accent1'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'slider',
                        label: 'Volume',
                        min: 0,
                        max: 1,
                        step: 0.01,
                        value: accent1.gain.value,
                        callback: value => {
                            accent1.gain.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Pan',
                        min: -1,
                        max: 1,
                        step: 0.01,
                        value: accent1.stereoPan.value,
                        callback: value => {
                            accent1.stereoPan.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Rate',
                        min: 0,
                        max: 2,
                        step: 0.01,
                        value: accent1.playbackRate.value,
                        callback: value => {
                            accent1.playbackRate.value = value;
                        }
                    }
                ].forEach(data => {
                    track3.add(new PanelItem(data));
                });

                const track4 = new Panel();
                track4.animateIn();
                this.ui.add(track4);

                [
                    {
                        label: 'Accent2'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'slider',
                        label: 'Volume',
                        min: 0,
                        max: 1,
                        step: 0.01,
                        value: accent2.gain.value,
                        callback: value => {
                            accent2.gain.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Pan',
                        min: -1,
                        max: 1,
                        step: 0.01,
                        value: accent2.stereoPan.value,
                        callback: value => {
                            accent2.stereoPan.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Rate',
                        min: 0,
                        max: 2,
                        step: 0.01,
                        value: accent2.playbackRate.value,
                        callback: value => {
                            accent2.playbackRate.value = value;
                        }
                    }
                ].forEach(data => {
                    track4.add(new PanelItem(data));
                });

                const track5 = new Panel();
                track5.animateIn();
                this.ui.add(track5);

                [
                    {
                        label: 'Kick'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'slider',
                        label: 'Volume',
                        min: 0,
                        max: 1,
                        step: 0.01,
                        value: kick.gain.value,
                        callback: value => {
                            kick.gain.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Pan',
                        min: -1,
                        max: 1,
                        step: 0.01,
                        value: kick.stereoPan.value,
                        callback: value => {
                            kick.stereoPan.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Rate',
                        min: 0,
                        max: 2,
                        step: 0.01,
                        value: kick.playbackRate.value,
                        callback: value => {
                            kick.playbackRate.value = value;
                        }
                    }
                ].forEach(data => {
                    track5.add(new PanelItem(data));
                });

                const track6 = new Panel();
                track6.animateIn();
                this.ui.add(track6);

                [
                    {
                        label: 'Snare'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'slider',
                        label: 'Volume',
                        min: 0,
                        max: 1,
                        step: 0.01,
                        value: snare.gain.value,
                        callback: value => {
                            snare.gain.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Pan',
                        min: -1,
                        max: 1,
                        step: 0.01,
                        value: snare.stereoPan.value,
                        callback: value => {
                            snare.stereoPan.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Rate',
                        min: 0,
                        max: 2,
                        step: 0.01,
                        value: snare.playbackRate.value,
                        callback: value => {
                            snare.playbackRate.value = value;
                        }
                    }
                ].forEach(data => {
                    track6.add(new PanelItem(data));
                });

                const track7 = new Panel();
                track7.animateIn();
                this.ui.add(track7);

                [
                    {
                        label: 'Hihat'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'slider',
                        label: 'Volume',
                        min: 0,
                        max: 1,
                        step: 0.01,
                        value: hihat.gain.value,
                        callback: value => {
                            hihat.gain.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Pan',
                        min: -1,
                        max: 1,
                        step: 0.01,
                        value: hihat.stereoPan.value,
                        callback: value => {
                            hihat.stereoPan.value = value;
                        }
                    },
                    {
                        type: 'slider',
                        label: 'Rate',
                        min: 0,
                        max: 2,
                        step: 0.01,
                        value: hihat.playbackRate.value,
                        callback: value => {
                            hihat.playbackRate.value = value;
                        }
                    }
                ].forEach(data => {
                    track7.add(new PanelItem(data));
                });
            }
        }

        class App {
            static async init() {
                this.initLoader();
                this.initViews();

                this.addListeners();

                await this.bufferLoader.ready();

                this.initAudio();
                this.initPanel();
            }

            static initLoader() {
                this.bufferLoader = new BufferLoader();
                this.bufferLoader.loadAll([
                    'assets/sounds/metal_monk_loop.mp3',
                    'assets/sounds/ethereal_bells.mp3',
                    'assets/sounds/accent_transition_1.mp3',
                    'assets/sounds/accent_transition_2.mp3',
                    'assets/sounds/hover.mp3',
                    'assets/sounds/click.mp3',
                    'assets/sounds/kick.mp3',
                    'assets/sounds/snare.mp3',
                    'assets/sounds/hihat.mp3'
                ]);
            }

            static initViews() {
                this.ui = new UI();
                document.body.appendChild(this.ui.element);
            }

            static initAudio() {
                WebAudio.init({ sampleRate: 48000 });
                WebAudio.load(this.bufferLoader.files);

                AudioController.init(this.ui.instructions);
            }

            static initPanel() {
                PanelController.init(this.ui);
            }

            static addListeners() {
                ticker.start();
            }
        }

        App.init();
    </script>
</head>
<body>
</body>
</html>
